import json
import os
import signal

import opcua
from opcua import Server
import socket
import errno
import time
from typing import Optional
import threading
from paho.mqtt.client import Client, Properties
import paho.mqtt.client as mqtt
import logging
from concurrent_log_handler import ConcurrentRotatingFileHandler

from paho.mqtt.enums import CallbackAPIVersion


LOGGER = {
    "LOGGER_NAME": "data_pedestal",
    "TOTAL_LEVEL": logging.INFO,
    "FILE_HANDLER_LEVEL": logging.DEBUG,
    "CONTROL_HANDLER_LEVEL": logging.INFO,
    "FILE_NAME": r"log.txt",
    "MODE": "a",
    "MAX_BYTES": 10 * 1024 * 1024,
    "BACKUP_COUNT": 10,
    "ENCODING": "UTF-8",
    "FORMAT": "%(asctime)s - %(filename)s[line:%(lineno)d] - %(levelname)s: %(message)s"
}

pid = os.getpid()


def get_logger() -> logging.getLogger():
    logger = logging.getLogger(LOGGER['LOGGER_NAME'])
    logger.setLevel(LOGGER['TOTAL_LEVEL'])  # 设置日志级别
    file_handler = ConcurrentRotatingFileHandler(filename=LOGGER['FILE_NAME'], mode=LOGGER['MODE'],
                                                 maxBytes=LOGGER['MAX_BYTES'],
                                                 backupCount=LOGGER['BACKUP_COUNT'],
                                                 encoding=LOGGER['ENCODING'])  # 设置输出文件
    file_handler.setLevel(LOGGER['FILE_HANDLER_LEVEL'])
    control_handler = logging.StreamHandler()
    control_handler.setLevel(LOGGER['CONTROL_HANDLER_LEVEL'])
    formatter = logging.Formatter(LOGGER['FORMAT'])
    file_handler.setFormatter(formatter)
    control_handler.setFormatter(formatter)
    logger.addHandler(file_handler)
    logger.addHandler(control_handler)
    return logger


# 创建一个实例
logger = get_logger()

try:
    with open('opcua_server_config.json', 'r') as f:
        config_txt = f.read()
except FileNotFoundError as e:
    logger.error(str(e))
try:
    config = json.loads(config_txt)
except Exception as e:
    logger.error(str(e))
    os.kill(pid, signal.SIGTERM)


class MQTTPublisher:

    def __init__(self, host: str, port: int):
        self.host = host
        self.port = port
        self._client = Client(callback_api_version=CallbackAPIVersion.VERSION2)
        self.connected = False
        self._lock = threading.Lock()
        self.connect()

    def connect(self):
        """
        和 EMQX 服务端建立连接, 如果失败则无限尝试
        """
        with self._lock:
            # 如果连接已经建立，那么直接 return
            if self.connected:
                return
            while True:
                try:
                    self._client.connect(
                        host=self.host,
                        port=self.port,
                    )
                    self.connected = True
                except (ConnectionRefusedError, socket.timeout):
                    logger.error("和 EMQX 建立连接失败，请检查 EMQX 服务是否启动, 端口是否开放")
                except OSError as e:
                    if e.errno == errno.EHOSTUNREACH:
                        logger.error("当前主机似乎处于一个没有网络的环境中")
                    else:
                        logger.error("当前主机的网络似乎存在问题")
                else:
                    break
                time.sleep(0.5)
            self._client.loop_start()

    def send_message(self, topic: str,
                     payload: Optional[bytes] = None,
                     qos: int = 0,
                     retain: bool = False,
                     properties: Optional[Properties] = None):
        """
        向 EMQX 发送消息
        :param topic: 主题
        :param payload: 消息体
        :param qos: 消息传输的质量级别, 有三个选项
                    0: 消息最多被传递一次, 不保证到达, 这是最快的传输方式，但也是最不可靠的
                    1: 消息至少被传递一次, 确保消息到达, 但可能会导致消息重复
                    2: 消息确保只被传递一次, 这是最可靠的传输方式, 但也是最慢的
        :param retain: 对于 EMQX 来说, 如果消费者后启动, 那么之前生产者发送的消息就接收不到了
                       因此可以将 retain 设置 True, 让 EMQX 保留起来, 但只能保留一条
        :param properties: 用于对消息进行更详细的属性设置
        :return:
        """
        while True:
            status = self._send_message(topic, payload, qos, retain, properties)
            if status == mqtt.MQTT_ERR_SUCCESS:  # 发送成功
                # 消息发送成功
                return
            elif status == mqtt.MQTT_ERR_NOMEM:  # 内存不足, 不应该再尝试了
                logger.error("EMQX 服务端已没有多余内存用于存储消息")
                return
            elif status in (mqtt.MQTT_ERR_NO_CONN, mqtt.MQTT_ERR_CONN_REFUSED, mqtt.MQTT_ERR_CONN_LOST):
                logger.error("因连接问题导致消息发送失败, 现重新建立连接")
                time.sleep(0.5)
                self.connect()  # 连接出现问题, 应当重新建立连接
            else:
                logger.error(f"非连接问题, 导致消息发送失败, 状态码为 {status}, 请联系管理员进行排查")
                return

    def _send_message(self, *args) -> int:
        """
        发送消息
        """
        message_info = self._client.publish(*args)
        # 消息状态
        status = message_info.rc
        return status

    def close(self):
        """
        和 MQTT 断开连接
        """
        self._client.disconnect()


mqtt_publisher = MQTTPublisher(config['EMQX']['host'], config['EMQX']['port'])


class SubHandler(object):
    def datachange_notification(self, node: opcua.Node, val, data):
        mqtt_publisher.connect()
        mqtt_publisher.send_message(
            topic=config['EMQX']['topic_name'],
            payload=json.dumps({f"{node.get_browse_name().Name}:{ node.nodeid.to_string()}": val}).encode('utf-8'),
        )


def opcua_server_run(endpoint, uri, address_space):
    """
    :param endpoint like opc.tcp://ip:port
    :param uri like https://www.baidu.com
    :param address_space lile {'folder_name': [tag_list in folder]}
    启动运行OPCUA服务的后台进程的任务
    :return:
    """
    # setup our server
    try:
        server = Server()
        server.set_endpoint(endpoint)

        # setup our own namespace, not really necessary but should as spec
        idx = server.register_namespace(uri)

        # get Objects node, this is where we should put our nodes
        objects = server.get_objects_node()

        # populating our address space
        _tag_list = []
        for folder, tag_list in address_space.items():
            # 创建文件夹
            _folder = objects.add_folder(idx, folder)
            # 创建文件夹中的tag list
            for tag in tag_list:
                _tag = _folder.add_variable(idx, tag, 0.0)  # default 0.0
                # 将变量设置为可写
                _tag.set_writable()
                _tag_list.append(_tag)
        # starting!
        server.start()
        handler = SubHandler()
        sub = server.create_subscription(100, handler)
        sub.subscribe_data_change(_tag_list)
    except Exception as e:
        logger.error(e)
        os.kill(pid, signal.SIGTERM)


def main():
    opcua_server_run(config["OPCUA"]["endpoint"], config["OPCUA"]["uri"], config["OPCUA"]["address_space"])


if __name__ == '__main__':
    main()
